P04-2. Zero-Trust 보안과 SPIFFE SVID 인증 인가 - 실습 검증
Part 04 (실습): Zero-Trust 보안과 SPIFFE/SVID 인증 · 인가 검증
- 워커 노드의
ztunnel메모리에 발급된 실제SPIFFEX.509 인증서(SVID) 목록을 조회하고,spiffe://cluster.local/ns/<NS>/sa/<SA>형식의 전자 여권 구조를 확인한다. PeerAuthentication(mode: STRICT) 정책을 배포해 네임스페이스 내 모든 평문 통신을 원천 차단하고 오직 mTLS(HBONE) 암호화 통신만 허용한다.- 인가되지 않은 침입자 파드(
rogue-client, 계정:rogue-sa)를 투입한 뒤,Waypoint Proxy가 집행하는 L7AuthorizationPolicy를 통해 계정별(principals), HTTP 메서드별(GETvsDELETE), URL 경로별(/ordersvs/admin/secret) 출입 통제(200 OKvs403 RBAC: access denied) 를 완벽히 검증한다. - 파드 IP로 직접 찌르는 우회 공격을 막기 위해
ztunnel이 직접 집행하는 L4AuthorizationPolicy까지 적용하고ztunnel의 실시간 차단 로그를 확인한다.
- P04-1 이론 개념을 먼저 읽고 오면
SPIFFE,PeerAuthentication,AuthorizationPolicy의 우선순위와 L4(ztunnel) vs L7(Waypoint) 집행 차이가 명확히 이해됩니다. - Part 03 실습을 마친 상태(
part1-demo네임스페이스에 Ambient 모드와waypoint프록시가 활성화된 상태)에서 바로 이어서 진행합니다. - Windows PowerShell 창을 열고 아래 명령어로 현재 파드 상태를 확인합니다.
$env:Path += ";$HOME\bin" cd "$HOME\mesh-study" kubectl get pods -n part1-demo
실습 아키텍처 및 Zero-Trust 검문 시나리오
이번 실습에서 검증할 4가지 출입 통제 시나리오입니다.
flowchart TB
subgraph CLIENTS["요청을 보내는 두 파드 (서로 다른 SPIFFE 여권 보유)"]
C_GOOD["study-client 파드
여권: spiffe://cluster.local/ns/part1-demo/sa/default"]
C_BAD["rogue-client 파드 (침입자!)
여권: spiffe://cluster.local/ns/part1-demo/sa/rogue-sa"]
end
subgraph GATEKEEPER["L7 검문소 · Waypoint Proxy (order-api-zero-trust 정책)"]
RULE["[허용 조건 (Whitelist)]
1. 출처(Principal): .../sa/default 만 허용
2. 메서드(Method): GET 만 허용
3. 금지 경로(notPaths): /admin* 제외"]
end
subgraph RESULTS["검문 결과"]
R1["시나리오 1: study-client -> GET /orders
결과: 200 OK 통과!"]
R2["시나리오 2: study-client -> GET /admin/secret
결과: 403 RBAC: access denied 차단!"]
R3["시나리오 3: study-client -> DELETE /orders
결과: 403 RBAC: access denied 차단!"]
R4["시나리오 4: rogue-client -> GET /orders
결과: 403 RBAC: access denied 차단!"]
end
C_GOOD --> RULE
C_BAD --> RULE
RULE --> R1
RULE --> R2
RULE --> R3
RULE --> R4Step 1. ztunnel 메모리에 발급된 SPIFFE 전자 여권(SVID) 실물 확인
istiod 컨트롤 플레인(내장 CA)이 각 워커 노드의 ztunnel에게 어떤 디지털 인증서를 내려보냈는지 istioctl ztunnel-config certificates 명령어로 확인합니다.
(이때 어느 노드의 ztunnel을 조회할지 --node <노드이름> 옵션을 지정합니다.)
# 1. desktop-worker 노드의 ztunnel이 보유 중인 SPIFFE 인증서 목록 조회
istioctl ztunnel-config certificates --node desktop-worker
# 2. JSON 출력에서 SPIFFE ID(identity) 필드만 발췌하여 확인
istioctl ztunnel-config certificates --node desktop-worker -o json | Select-String "spiffe://" -Context 2,2
실제 출력 결과:
CERTIFICATE NAME TYPE STATUS VALID CERT SERIAL NUMBER NOT AFTER NOT BEFORE
spiffe://cluster.local/ns/part1-demo/sa/default Leaf Available true 18e2aac1fbba1042a49b622055891b69 2026-10-26T07:05:02Z 2026-10-25T07:03:02Z
spiffe://cluster.local/ns/part1-demo/sa/default Root Available true f1d9603bf93bcada5f16cdd5b361f9e4 2036-10-22T06:50:42Z 2026-10-25T06:50:42Z
[
{
> "identity": "spiffe://cluster.local/ns/part1-demo/sa/default",
"state": "Available",
"certChain": [
},
출력 결과 상세 해부:
CERTIFICATE NAME: spiffe://cluster.local/ns/part1-demo/sa/default:
desktop-worker노드 위에서 돌아가고 있는part1-demo파드들(study-client,order-api)이 기본 서비스 계정(default)을 사용하고 있으므로, 노드의ztunnel이istiod로부터 해당 계정의 전자 여권을 미리 받아 메모리에 들고 있습니다!TYPE: LeafvsRoot:Leaf: 실제 파드의 신분을 증명하는 단말 인증서이며,NOT BEFORE와NOT AFTER를 보면 유효기간이 정확히 24시간(1일) 입니다.Root: 이 인증서에 도장을 찍어준istiod최상위 인증기관(CA)의 뿌리 인증서(유효기간 10년)입니다.
Step 2. 침입자 파드(rogue-client), STRICT mTLS 인증, L7 인가 정책 배포
이제 Zero-Trust 보안을 검증하기 위해 3가지를 한 번에 배포합니다:
PeerAuthentication(strict-mtls):part1-demo네임스페이스 안의 모든 통신에 엄격한(STRICT) mTLS 암호화를 강제합니다.- 침입자 계정(
rogue-sa)과 파드(rogue-client): 해커가 클러스터 내부의 다른 파드(rogue-sa계정 사용)를 장악해order-api를 몰래 호출하는 상황을 재현합니다. - L7
AuthorizationPolicy(order-api-zero-trust):order-api서비스(targetRefs: Service/order-api) 앞단의Waypoint Proxy에게 다음 규칙을 하달합니다:- 오직
cluster.local/ns/part1-demo/sa/default신분증을 가진 파드만(principals), - 오직
GET메서드로만(methods), - 단,
/admin*으로 시작하는 관리자 경로는 제외하고(notPaths) 접근을 허용(ALLOW)하라! (나머지는 전부 자동 차단!)
- 오직
# part4-zero-trust.yaml 파일 생성 및 클러스터 적용
@"
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
name: strict-mtls
namespace: part1-demo
spec:
mtls:
mode: STRICT
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rogue-sa
namespace: part1-demo
---
apiVersion: v1
kind: Pod
metadata:
name: rogue-client
namespace: part1-demo
labels:
app: rogue-client
spec:
serviceAccountName: rogue-sa
containers:
- name: rogue-client
image: curlimages/curl:8.10.1
command: ["sleep", "3650d"]
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: order-api-zero-trust
namespace: part1-demo
spec:
targetRefs:
- kind: Service
group: ""
name: order-api
action: ALLOW
rules:
- from:
- source:
principals: ["cluster.local/ns/part1-demo/sa/default"]
to:
- operation:
methods: ["GET"]
notPaths: ["/admin*"]
"@ | Set-Content -Path "$HOME\mesh-study\part4-zero-trust.yaml" -Encoding utf8
kubectl apply -f "$HOME\mesh-study\part4-zero-trust.yaml"
kubectl wait --for=condition=Ready pod/rogue-client -n part1-demo --timeout=90s
실제 출력 결과:
peerauthentication.security.istio.io/strict-mtls created
serviceaccount/rogue-sa created
pod/rogue-client created
authorizationpolicy.security.istio.io/order-api-zero-trust created
pod/rogue-client condition met
principals를 적을 때 주의할 점!
AuthorizationPolicy의 principals 칸에 SPIFFE ID를 적을 때는 맨 앞의 spiffe:// 접두사를 빼고 "cluster.local/ns/part1-demo/sa/default" 형태로 적는 것이 Istio의 문법 규칙입니다!
잠시 istioctl ztunnel-config certificates --node desktop-worker를 다시 쳐 보면, 새로 뜬 rogue-client 파드를 위해 spiffe://cluster.local/ns/part1-demo/sa/rogue-sa 인증서도 즉시 발급된 것을 볼 수 있습니다!
Step 3. 4대 출입 통제 시나리오 실측 검증 (200 OK vs 403 Forbidden)
이제 네 가지 요청을 차례대로 던져서 Waypoint Proxy가 우리의 Zero-Trust 헌법대로 정확히 통과/차단하는지 확인합니다.
# [시나리오 1] 정당한 신분(sa/default) + 허용된 메서드(GET) + 일반 경로(/orders) -> 200 OK 통과!
kubectl exec -n part1-demo study-client -- curl -s -w "`nHTTP_CODE: %{http_code}`n" http://order-api/orders | Select-Object -Last 6
# [시나리오 2] 정당한 신분(sa/default)이지만 금지된 관리자 경로(GET /admin/secret) -> 403 차단!
kubectl exec -n part1-demo study-client -- curl -s -w "`nHTTP_CODE: %{http_code}`n" http://order-api/admin/secret
# [시나리오 3] 정당한 신분(sa/default)이지만 허용되지 않은 메서드(DELETE /orders) -> 403 차단!
kubectl exec -n part1-demo study-client -- curl -s -X DELETE -w "`nHTTP_CODE: %{http_code}`n" http://order-api/orders
# [시나리오 4] 허가받지 않은 침입자 신분(sa/rogue-sa)의 접근(GET /orders) -> 403 차단!
kubectl exec -n part1-demo rogue-client -- curl -s -w "`nHTTP_CODE: %{http_code}`n" http://order-api/orders
실제 출력 결과:
# [시나리오 1 결과]
"os": {
"hostname": "order-api-86d8bb68dd-xh5rm"
},
"connection": {}
}
HTTP_CODE: 200
# [시나리오 2 결과]
RBAC: access denied
HTTP_CODE: 403
# [시나리오 3 결과]
RBAC: access denied
HTTP_CODE: 403
# [시나리오 4 결과]
RBAC: access denied
HTTP_CODE: 403
order-api컨테이너(Node.js)는 인증서 검증 코드도, 권한 체크 코드도 전혀 갖고 있지 않습니다.- 하지만 앞단의
Waypoint Proxy가 mTLS 인증서 속 SPIFFE 신분증과 HTTP 메서드, URL 경로를 대조하여 시나리오 2, 3, 4번을 모두RBAC: access denied(HTTP_CODE: 403) 로 완벽히 막아냈습니다!
Step 4. 우회 공격 방어 — ztunnel의 L4 AuthorizationPolicy 직접 집행 검증
만약 똑똑한 해커(rogue-client)가 "어? 쿠버네티스 Service 도메인(http://order-api:80)으로 찌르니까 Waypoint Proxy가 막네? 그럼 Waypoint를 안 거치고 order-api 파드의 실제 IP 주소(10.244.x.x:8080)로 직접 찌르면 뚫리지 않을까?" 하고 시도하면 어떻게 될까요?
이를 방어하기 위해 파드 자체(selector.matchLabels: app: order-api)에 걸리는 L4 AuthorizationPolicy 를 추가하면, 목적지 워커 노드의 ztunnel이 L4(TCP/HBONE) 단계에서 연결 자체를 아예 끊어버립니다! 직접 확인해 봅시다.
# 1. order-api 파드를 직접 보호하는 L4 DENY 정책 생성 (ztunnel이 직접 집행!)
@"
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
name: order-api-l4-ztunnel
namespace: part1-demo
spec:
selector:
matchLabels:
app: order-api
action: DENY
rules:
- from:
- source:
principals: ["cluster.local/ns/part1-demo/sa/rogue-sa"]
"@ | kubectl apply -f -
# 2. rogue-client가 Service(Waypoint)를 우회해 order-api 파드의 실제 IP(:8080)로 직접 접속 시도!
$orderPodIP = kubectl get pod -n part1-demo -l app=order-api -o jsonpath="{.items[0].status.podIP}"
kubectl exec -n part1-demo rogue-client -- curl -s -w "`nEXIT_CODE: %{exitcode}`n" --max-time 3 "http://${orderPodIP}:8080/orders"
# 3. ztunnel 로그에서 L4 정책 거부(policy rejection) 현장 포착!
kubectl logs -n istio-system -l app=ztunnel --tail=5 | Select-String "order-api-l4-ztunnel"
실제 출력 결과:
authorizationpolicy.security.istio.io/order-api-l4-ztunnel created
EXIT_CODE: 56
command terminated with exit code 56
2026-10-25T07:08:44.211072Z error access connection complete src.addr=10.244.2.9:52256 src.workload="rogue-client" src.namespace="part1-demo" src.identity="spiffe://cluster.local/ns/part1-demo/sa/rogue-sa" src.cluster="Kubernetes" dst.addr=10.244.2.6:15008 dst.hbone_addr=10.244.2.6:8080 dst.service="order-api.part1-demo.svc.cluster.local" dst.workload="order-api-86d8bb68dd-mkvrw" dst.namespace="part1-demo" dst.identity="spiffe://cluster.local/ns/part1-demo/sa/default" dst.cluster="Kubernetes" direction="inbound" bytes_sent=0 bytes_recv=0 duration="0ms" error="connection closed due to policy rejection: explicitly denied by: part1-demo/order-api-l4-ztunnel"
curl이EXIT_CODE: 56(연결 강제 종료)으로 즉시 튕겨 나갔고,ztunnel로그에bytes_sent=0 bytes_recv=0 duration="0ms" error="connection closed due to policy rejection: explicitly denied by: part1-demo/order-api-l4-ztunnel"이라는 명확한 L4 차단 기록이 남았습니다! 단 1바이트의 데이터도 파드에 닿기 전에 커널/노드 입구에서 완벽히 차단된 것입니다.
다음 파트 실습을 위한 보안 정책 정리
Part 05에서 자유롭게 카나리 배포와 서킷 브레이커 실험을 할 수 있도록, 방금 건 인가 정책 2개와 rogue-client를 깔끔히 정리합니다(strict-mtls는 그대로 유지해도 좋습니다!).
kubectl delete authorizationpolicy order-api-l4-ztunnel order-api-zero-trust -n part1-demo
kubectl delete pod rogue-client -n part1-demo
Step 5. 자주 발생하는 문제와 해결 (Troubleshooting)
| 증상 / 에러 메시지 | 원인 | 해결 방법 |
|---|---|---|
istioctl ztunnel-config certificates 실행 시 Error: ztunnel pod name or --node must be set 발생 |
클러스터에 노드가 3대이므로 어느 노드의 ztunnel 메모리를 볼지 지정해야 함 |
istioctl ztunnel-config certificates --node desktop-worker 처럼 노드 이름을 지정 |
AuthorizationPolicy에 principals: ["spiffe://cluster.local/..."]로 적었더니 허용이 안 되고 403이 뜸 |
Istio AuthorizationPolicy의 principals 필드는 spiffe:// 스킴을 제외한 나머지 문자열을 매칭함 |
앞의 spiffe://를 빼고 "cluster.local/ns/part1-demo/sa/default" 로 작성 |
Waypoint가 없는 상태에서 AuthorizationPolicy에 methods: ["GET"]을 넣었더니 모든 요청이 막힘 |
ztunnel은 L4 프록시라 HTTP 메서드를 읽을 수 없어 보안상 안전하게 전체 차단(Fail-Close)함 |
L7 조건을 쓰려면 targetRefs로 Service를 지정하고 해당 네임스페이스/서비스에 Waypoint를 활성화 |
Step 6. 이번 파트 핵심 명령어 정리 (Command Reference)
# 1. 특정 워커 노드의 ztunnel이 메모리에 들고 있는 SPIFFE 인증서(SVID) 목록 확인
istioctl ztunnel-config certificates --node desktop-worker
# 2. 네임스페이스 내의 PeerAuthentication 및 AuthorizationPolicy 목록 조회
kubectl get peerauthentication,authorizationpolicy -n part1-demo
# 3. ztunnel에서 L4 RBAC 정책 적용 및 차단 로그 확인
kubectl logs -n istio-system -l app=ztunnel --tail=20 | Select-String "policy rejection"
Step 7. 셀프 체크 (Self-Check Q&A)
Q1. 쿠버네티스 환경에서 출발지 파드의 IP 주소(10.244.x.x) 대신 SPIFFE ID를 기준으로 접근 제어를 해야 하는 이유는 무엇인가요?
쿠버네티스 파드는 재시작이나 오토스케일링 때마다 수시로 삭제되고 새로 생성되며 IP 주소가 계속 바뀌기(휘발성) 때문입니다. 반면 SPIFFE ID(spiffe://cluster.local/ns/<NS>/sa/<SA>)는 파드의 ServiceAccount에 기반하여 istiod CA가 X.509 인증서(SAN 필드)로 서명해 발급하므로 IP가 바뀌어도 파드의 신분을 암호학적으로 100% 보장합니다.
Q2. 특정 서비스에 action: ALLOW인 AuthorizationPolicy를 딱 1개 추가하면, 그 정책에 언급되지 않은 다른 파드들의 요청은 어떻게 처리되나요?
전면 차단(Default Deny) 됩니다. ALLOW 정책이 하나도 없을 때는 모두 허용되지만, 단 1개라도 등록되는 순간 화이트리스트(Whitelist) 모드로 전환되어 명시적으로 허용된 조건 외의 모든 접근을 거부합니다.
Q3. Ambient Mesh에서 Waypoint Proxy가 집행하는 L7 차단과 ztunnel이 집행하는 L4 차단은 클라이언트 입장에서 어떤 결과 차이를 보이나요?
Waypoint Proxy(L7) 가 차단할 때는 HTTP 계층을 이해하므로 정식 HTTP 403 Forbidden (RBAC: access denied) 응답을 돌려줍니다. 반면 ztunnel(L4) 이 차단할 때는 TCP/HBONE 연결 수립 단계에서 즉시 소켓을 닫아버리므로(connection closed due to policy rejection) 클라이언트는 curl exit code 56 (연결 리셋) 에러를 받게 됩니다.
🔗 다음 파트로 이동
이제 우리 메시는 사이드카 없이도 강력한 Zero-Trust 보안(STRICT mTLS + L4/L7 RBAC) 까지 갖추었습니다!
다음 Part 05에서는 서비스 메시의 꽃이라 불리는 L7 트래픽 엔지니어링(90:10 카나리 배포, 헤더 기반 라우팅, 결함 주입)과 서킷 브레이커(OutlierDetection & UO) 를 실습합니다.
네비게이션
| 이전 | 목록 | 다음 |
|---|---|---|
| ◀ Part 04 이론 | 목록으로 | Part 05 이론 ▶ |